/*
 * JWT工具类
 * 位于包：org.example.lethear.utils
 * 
 * 这个类是JWT令牌处理的核心工具类，负责：
 * 1. 生成访问令牌（Access Token）
 * 2. 生成刷新令牌（Refresh Token）
 * 3. 验证令牌的有效性
 * 4. 从令牌中提取用户信息
 * 5. 计算令牌的剩余有效时间
 * 
 * JWT令牌结构：Header.Payload.Signature
 * - Header：包含令牌类型和签名算法
 * - Payload：包含用户信息和其他声明
 * - Signature：用于验证令牌的完整性
 */
package org.example.lethear.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * JWT工具类
 * 
 * 使用HMAC-SHA256算法对JWT令牌进行签名和验证
 * 支持访问令牌和刷新令牌的生成与验证
 */
@Component
public class JwtUtils {
    
    // 从配置文件中注入JWT密钥
    // 这个密钥用于签名和验证JWT令牌，必须保密
    @Value("${jwt.secret}")
    private String secret;
    
    // 从配置文件中注入访问令牌的过期时间（毫秒）
    // 通常设置为较短的时间，如1小时
    @Value("${jwt.expiration}")
    private Long expiration;
    
    // 从配置文件中注入刷新令牌的过期时间（毫秒）
    // 通常设置为较长的时间，如7天或30天
    @Value("${jwt.refresh-expiration}")
    private Long refreshExpiration;

    /**
     * 获取用于签名的密钥
     * 
     * 将字符串密钥转换为SecretKey对象
     * 使用HMAC-SHA256算法需要至少256位（32字节）的密钥
     * 
     * @return SecretKey 签名密钥对象
     */
    private SecretKey getSigningKey() {
        // 1. 将字符串密钥转换为字节数组
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        
        // 2. 使用HMAC-SHA256算法创建密钥对象
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    /**
     * 从JWT令牌中提取用户名
     * 
     * JWT的subject字段通常用于存储用户名
     * 
     * @param token JWT令牌字符串
     * @return String 用户名，如果令牌无效则可能抛出异常
     */
    public String getUsernameFromToken(String token) {
        // 使用getClaimFromToken方法提取subject声明
        // Claims::getSubject是一个方法引用，等价于 claims -> claims.getSubject()
        return getClaimFromToken(token, Claims::getSubject);
    }
    
    /**
     * 从JWT令牌中提取过期时间
     * 
     * @param token JWT令牌字符串
     * @return Date 令牌的过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        // 使用getClaimFromToken方法提取expiration声明
        return getClaimFromToken(token, Claims::getExpiration);
    }
    
    /**
     * 从JWT令牌中提取指定的声明（Claim）
     * 
     * 这是一个泛型方法，可以提取任何类型的声明
     * 
     * @param token JWT令牌字符串
     * @param claimsResolver 声明解析器，用于从Claims对象中提取特定的声明
     * @return T 指定类型的声明值
     */
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        // 1. 获取所有声明
        final Claims claims = getAllClaimsFromToken(token);
        
        // 2. 使用解析器从声明中提取特定的值
        return claimsResolver.apply(claims);
    }
    
    /**
     * 从JWT令牌中提取所有声明
     * 
     * 这是解析JWT令牌的核心方法
     * 
     * @param token JWT令牌字符串
     * @return Claims 包含所有声明的对象
     * @throws JwtException 如果令牌格式错误或签名验证失败
     */
    private Claims getAllClaimsFromToken(String token) {
        // 1. 创建JWT解析器
        return Jwts.parser()
                // 2. 设置验证签名的密钥
                .verifyWith(getSigningKey())
                // 3. 构建解析器
                .build()
                // 4. 解析签名的JWT令牌
                .parseSignedClaims(token)
                // 5. 获取令牌的载荷（Payload）
                .getPayload();
    }
    
    /**
     * 检查JWT令牌是否已过期
     * 
     * @param token JWT令牌字符串
     * @return Boolean true表示已过期，false表示未过期
     */
    private Boolean isTokenExpired(String token) {
        // 1. 获取令牌的过期时间
        final Date expiration = getExpirationDateFromToken(token);
        
        // 2. 比较过期时间与当前时间
        // before方法：如果过期时间在当前时间之前，则返回true（已过期）
        return expiration.before(new Date());
    }

    /**
     * 为用户生成访问令牌（带额外声明）
     * 
     * @param userDetails 用户详情对象
     * @param extraClaims 额外的声明，如用户ID、角色等
     * @return String 生成的JWT令牌
     */
    public String generateToken(UserDetails userDetails, Map<String, Object> extraClaims) {
        // 1. 创建声明映射，包含额外声明
        Map<String, Object> claims = new HashMap<>(extraClaims);
        
        // 2. 调用createToken方法生成令牌
        return createToken(claims, userDetails.getUsername());
    }
    
    /**
     * 生成刷新令牌
     * 
     * 刷新令牌用于获取新的访问令牌，通常有效期较长
     * 
     * @param userDetails 用户详情对象
     * @return String 生成的刷新令牌
     */
    public String generateRefreshToken(UserDetails userDetails) {
        // 1. 创建声明映射，标识这是一个刷新令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put("type", "refresh");
        
        // 2. 调用createRefreshToken方法生成刷新令牌
        return createRefreshToken(claims, userDetails.getUsername());
    }
    
    /**
     * 创建访问令牌
     * 
     * 这是生成JWT令牌的核心方法
     * 
     * @param claims 要包含在令牌中的声明
     * @param subject 令牌的主题（通常是用户名）
     * @return String 生成的JWT令牌
     */
    private String createToken(Map<String, Object> claims, String subject) {
        // 1. 获取当前时间作为签发时间
        Date now = new Date();
        
        // 2. 计算过期时间（当前时间 + 过期时长）
        Date expiryDate = new Date(now.getTime() + expiration);
        
        // 3. 使用JWT构建器创建令牌
        return Jwts.builder()
                // 4. 设置自定义声明
                .claims(claims)
                // 5. 设置主题（用户名）
                .subject(subject)
                // 6. 设置签发时间
                .issuedAt(now)
                // 7. 设置过期时间
                .expiration(expiryDate)
                // 8. 使用密钥签名
                .signWith(getSigningKey())
                // 9. 构建并返回令牌字符串
                .compact();
    }
    
    /**
     * 创建刷新令牌
     * 
     * 刷新令牌的创建过程与访问令牌类似，但使用不同的过期时间
     * 
     * @param claims 要包含在令牌中的声明
     * @param subject 令牌的主题（通常是用户名）
     * @return String 生成的刷新令牌
     */
    private String createRefreshToken(Map<String, Object> claims, String subject) {
        // 1. 获取当前时间作为签发时间
        Date now = new Date();
        
        // 2. 计算过期时间（当前时间 + 刷新令牌过期时长）
        Date expiryDate = new Date(now.getTime() + refreshExpiration);
        
        // 3. 使用JWT构建器创建刷新令牌
        return Jwts.builder()
                // 4. 设置自定义声明
                .claims(claims)
                // 5. 设置主题（用户名）
                .subject(subject)
                // 6. 设置签发时间
                .issuedAt(now)
                // 7. 设置过期时间
                .expiration(expiryDate)
                // 8. 使用密钥签名
                .signWith(getSigningKey())
                // 9. 构建并返回令牌字符串
                .compact();
    }
    
    /**
     * 验证JWT令牌（需要用户详情）
     * 
     * 这个方法进行全面的令牌验证：
     * 1. 验证令牌格式和签名
     * 2. 检查令牌是否过期
     * 3. 验证令牌中的用户名是否与提供的用户详情匹配
     * 
     * @param token JWT令牌字符串
     * @param userDetails 用户详情对象
     * @return Boolean true表示令牌有效，false表示无效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        try {
            // 1. 从令牌中提取用户名
            final String username = getUsernameFromToken(token);
            
            // 2. 验证用户名是否匹配且令牌未过期
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
            
        } catch (Exception e) {
            // 3. 如果发生任何异常（格式错误、签名验证失败等），记录错误并返回false
            System.err.println("JWT token validation error: " + e.getMessage());
            return false;
        }
    }
    
    /**
     * 验证JWT令牌（不需要用户详情）
     * 
     * 这个方法只验证令牌的基本有效性：
     * 1. 验证令牌格式和签名
     * 2. 检查令牌是否过期
     * 
     * @param token JWT令牌字符串
     * @return Boolean true表示令牌有效，false表示无效
     */
    public Boolean validateToken(String token) {
        try {
            // 1. 尝试解析令牌，如果格式错误或签名验证失败会抛出异常
            getAllClaimsFromToken(token);
            
            // 2. 检查令牌是否过期
            return !isTokenExpired(token);
            
        } catch (Exception e) {
            // 3. 如果发生任何异常，记录错误并返回false
            System.err.println("JWT token validation error: " + e.getMessage());
            return false;
        }
    }

    /**
     * 获取JWT令牌的剩余有效时间
     * 
     * 这个方法用于计算令牌还有多长时间过期
     * 前端可以使用这个信息来决定是否需要刷新令牌
     * 
     * @param token JWT令牌字符串
     * @return Long 剩余有效时间（毫秒），如果令牌无效则返回0
     */
    public Long getTokenRemainingTime(String token) {
        try {
            // 1. 获取令牌的过期时间
            Date expiration = getExpirationDateFromToken(token);
            
            // 2. 计算剩余时间：过期时间 - 当前时间
            return expiration.getTime() - System.currentTimeMillis();
            
        } catch (Exception e) {
            // 3. 如果发生异常，记录错误并返回0
            System.err.println("Error getting token remaining time: " + e.getMessage());
            return 0L;
        }
    }
}